package com.maiyuren.fishkit.dict

import com.maiyuren.fishkit.utils.FishAssetsFileUtils
import com.maiyuren.fishkit.utils.FishZipUtils
import com.maiyuren.fishkit.utils.toInt32LE
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.io.RandomAccessFile

private const val FISHDICT_FILE_ID = "__FISHDICT__"

class FishDict(val cfg: FishDictCfg) {
    // 暂时先不处理分组
    // val groups: Array<FishDictWordGroup> = emptyArray();
    
    val name = cfg.name

    private data class CacheFileItem(
        val offset: Int,
        val length: Int
    )

    private data class KV<T> (
        val key: String,
        val value: T
    )

    private enum class LoadStatus {
        NotLoaded,
        Loading,
        LoadFailed,
        Loaded
    }

    private var hasJs_ = -1;
    private var hasCss_ = -1;
    private var dictHeaderInfo_: MutableMap<String, CacheFileItem>? = null;
    private var file_ : RandomAccessFile? = null; // todo: how to close
    private var config_: JSONObject? = null
    private var index_: MutableList<FishDictWord>? = null;
    private var replace_: MutableList<KV<String>>? = null
    private var redirect_: MutableList<FishDictRedirect>? = null
    private var loadStatus_:LoadStatus = LoadStatus.NotLoaded

    // 文字，或正則
    fun lookfor(wordOrReg: String, isReg:Boolean = false, maxCount:Int = 20): FishDictLookforResult {
        if (isWeb()) {
            return lookforWeb(word = wordOrReg)
        }

        // index
        if (!isReg) {
            try {
                val tempIndex = wordOrReg.toInt()
                if (tempIndex > 0) {
                    return lookforIndex(tempIndex)
                }
            } catch (e: NumberFormatException) {
            }

            // lookfor string
            return lookforString(
                word2lookfor = wordOrReg,
                maxCount = maxCount
                )
        }

        return lookforReg(
            reg = Regex(wordOrReg),
            maxCount = maxCount)
    }

    private fun hasCss() : Boolean {
        if (hasCss_ == -1) {
            hasCss_ = if (readFileBytes("assets/index.css") != null) 1 else 0
        }
        return hasCss_ > 0
    }

    private fun hasJs(): Boolean {
        if (hasJs_ == -1) {
            hasJs_ = if (readFileBytes("assets/index.js") != null) 1 else 0
        }
        return hasJs_ > 0
    }

    fun loadWordContent(r: FishDictWord, redirectResult: FishDictRedirectResult? = null): String? {
        loadWordContentInner(r);
        if (r.content == null) return null
        if (redirectResult != null) return redirectResult.template + "<br>" + r.content;
        return r.content
    }

    private fun loadWordContentInner(r: FishDictWord) {
        if (r.content != null) return

        val buf = readFileBytes(
            filePath = "content.data",
            offset = r.offset,
            len = r.length)
        if (buf == null) return

        // unzip
        var temp = FishZipUtils.unzipUtf8Buffer(buf)

        // replace
        temp = replaceContent(temp)

        if (hasCss()) temp += "\n<link href=\"assets/index.css\" rel=\"stylesheet\">"
        if (hasJs()) temp += "\n<script src=\"assets/index.js\"></script>"

        r.content = temp
    }

    private fun replaceContent(str: String): String {
        if (replace_ == null || replace_!!.size == 0) return str
        var i = replace_!!.size - 1;
        var r = str;
        while (i >= 0) {
            val item = replace_!![i]
            r = r.replace(item.key, item.value)
            --i;
        }
        return r;
    }

    private fun checkLoaded(): FishDictLookforResult? {
//        load();
        if (loadStatus_ == LoadStatus.LoadFailed) return FishDictLookforResult(errorMsg = FishDictLookforResultError.loadedFail)
        if (loadStatus_ == LoadStatus.NotLoaded || loadStatus_ == LoadStatus.Loading) return FishDictLookforResult(errorMsg = FishDictLookforResultError.loading)
        return null
    }

    private fun lookforWeb(word: String): FishDictLookforResult {
        val r = FishDictLookforResult()

        r.words.add(FishDictWord(
            index = 0,
            word = word,
            content = cfg.path.replace("\${word}", word),
            offset = 0,
            length = 0
            )
        )

        return r;
    }

    fun lookforIndex(index: Int): FishDictLookforResult {
        val errResult = checkLoaded();
        if (errResult != null) return errResult
        if (index <= 0 || index > index_!!.size) {
            return FishDictLookforResult(errorMsg = FishDictLookforResultError.outofRange
                .replace("MAX", index_!!.size.toString()))
        }
        val wordItem = index_!![index - 1].copy()
        wordItem.word = index.toString()
        val r = FishDictLookforResult()
        r.words.add(wordItem)
        loadWordContent(wordItem)
        return r
    }

    private fun getRedirectWord(word: String): FishDictRedirectResult? {
        if (redirect_ == null || redirect_!!.size == 0) return null
        for(i in 0 until redirect_!!.size) {
            val ri = redirect_!![i]
            val riResult = ri.lookfor(word)
            if (riResult != null) return riResult
        }

        return null
    }

    private fun lookforString(word2lookfor: String, maxCount:Int = 20): FishDictLookforResult {
        val errResult = checkLoaded();
        if (errResult != null) return errResult
        // redirect
        val redirectInfo = getRedirectWord(word2lookfor)

        val trueWordForLookfor = if (redirectInfo == null) word2lookfor else redirectInfo.to

        val r = FishDictLookforResult(redirectResult = redirectInfo)
        for(i in 0 until index_!!.size) {
            val item = index_!![i]
            if (item.word.startsWith(trueWordForLookfor) ||
                trueWordForLookfor.startsWith(item.word)) {
                r.words.add(item)
                if (r.words.size >= maxCount) break
            }
        }

        if (redirectInfo != null) {
            r.redirectResult = redirectInfo
        }

        r.words.sortWith(Comparator {u1, u2->
            if (u1.word.length == word2lookfor.length) return@Comparator -1
            if (u2.word.length == word2lookfor.length) return@Comparator 1
            if (u1.word.length < word2lookfor.length) return@Comparator 1
            if (u2.word.length > word2lookfor.length) return@Comparator -1

            val pos_1 = u1.word.indexOf(word2lookfor)
            val pos_2 = u2.word.indexOf(word2lookfor)
            if (pos_1 < pos_2) return@Comparator -1;
            if (pos_1 > pos_2) return@Comparator 1;
            return@Comparator u1.word.length - u2.word.length
        })

        if (r.words.size == 0) {
            r.errorMsg = FishDictLookforResultError.notFound
        }

        return r;
    }

    private fun lookforReg(reg: Regex, maxCount:Int = 20): FishDictLookforResult {
        val errResult = checkLoaded();
        if (errResult != null) return errResult

        val r = FishDictLookforResult()
        for(i in 0 until index_!!.size) {
            val item = index_!![i]
            if (reg.matches(item.word)) {
                r.words.add(item)
                if (r.words.size >= maxCount) break
            }
        }

        if (r.words.size == 0) {
            r.errorMsg = FishDictLookforResultError.notFound
        }

        return r;
    }


    fun isWeb(): Boolean {
        return cfg.type == FishDictType.web
    }

    fun load() {
        if (loadStatus_ != LoadStatus.NotLoaded) return
        if (isWeb()) {
            loadStatus_ = LoadStatus.Loaded;
            return
        }
        loadStatus_ = LoadStatus.Loading

        GlobalScope.launch {
            val configStr = readFileString("config.json")
            config_ = JSONObject(configStr)
            readIndex()
            readReplace()
            readRedirect()
            loadStatus_ = LoadStatus.Loaded;
        }
    }

    fun readFileBytes(filePath: String, offset: Int = 0, len: Int = 0): ByteArray? {
        this.readDictHeader()
        if (this.dictHeaderInfo_ == null || this.file_ == null) return null
        val foundInfo = this.dictHeaderInfo_!![filePath.lowercase()]
        if (foundInfo == null) return null
        val r_offset = offset
        var r_len = len
        if (r_len == 0) r_len = foundInfo.length
        val buf = ByteArray(r_len)
        val file_offset = r_offset + foundInfo.offset
        if (file_offset > 0) file_!!.seek(file_offset.toLong())
        val readLen = file_!!.read(buf)
        if (readLen != r_len) return null
        return buf
    }

    private fun readFileString(filePath: String, offset: Int = 0, len: Int = 0): String? {
        return this.readFileBytes(filePath = filePath, offset = offset, len = len)?.toString(Charsets.UTF_8)
    }

    private fun readDictHeader() {
        if (dictHeaderInfo_ != null) return
        if (file_ == null) file_ = FishAssetsFileUtils.open(cfg.path)
        if (file_ == null) return;

        var offset = 0;

        val buf = ByteArray(FISHDICT_FILE_ID.length)
        val readLen = file_?.read(buf)
        if (readLen != FISHDICT_FILE_ID.length) return
        if (buf.toString(Charsets.UTF_8) != FISHDICT_FILE_ID) return
        offset += buf.size

        val lenBuf = ByteArray(4)
        val readLenBufLen = file_?.read(lenBuf);
        if (readLenBufLen != lenBuf.size) return
        offset += readLenBufLen

        val headLen = lenBuf.toInt32LE();
        val headBuf = ByteArray(headLen)
        val readHeadBufLen = file_?.read(headBuf)
        if (readHeadBufLen != headLen) return
        offset += headLen

        val headStr = headBuf.toString(Charsets.UTF_8)
        val headArr = headStr.split("\n")
        dictHeaderInfo_ = mutableMapOf()
        headArr.forEach { line ->
            val kv = line.split(":")
            if (kv.size == 2) {
                val item = CacheFileItem(
                    offset = offset,
                    length = kv[1].toInt()
                )
                dictHeaderInfo_!![kv[0].lowercase()] = item
                offset += item.length
            }
        }
    }

    private fun readIndex() {
        val allIndexStr = this.readFileString("index.data")
        if (allIndexStr == null) return
        var offset = 0
        var group = ""
        index_ = mutableListOf()

        var begin = 0;
        while(true) {
            var len = 0;
            var word = ""
            var wordMoreInfo = ""
            // todo:
            // 字符串查詢太慢了。
            // 需要轉換成二進制
            // COUNT:LEN-WORD LEN-WORD LEN-WORD
            // MOREINFO_LEN-MOREINFO
            // LEN

//            val wordSplitArr = item.split(":")
//            if (wordSplitArr.size < 2) return@forEachIndexed
//            if (wordSplitArr[1].isEmpty()) { // group
//                group = wordSplitArr[0]
//                return@forEachIndexed
//            }
//
//            word = wordSplitArr[0]
//            val moreInfoSplitArr = wordSplitArr[1].split(",")
//            len = moreInfoSplitArr[0].toInt()
//            if (moreInfoSplitArr.size > 1) {
//                wordMoreInfo = moreInfoSplitArr[1]
//            }
            val pos = allIndexStr.indexOf(':', begin)
            if (pos == -1) break
            if (pos == allIndexStr.length - 1) break
            if (allIndexStr[pos + 1] == '\n') {
                group = allIndexStr.substring(begin, pos)
                begin = pos + 2 // :\n
                continue
            }
            word = allIndexStr.substring(begin, pos)
            var wordEndPos = allIndexStr.indexOf('\n', pos + 1)
            if (wordEndPos == -1) wordEndPos = allIndexStr.length
            len = allIndexStr.substring(pos + 1, wordEndPos).toInt()

            val newItem = FishDictWord(
                index = index_!!.size + 1,
                word = word,
                wordMoreInfo = wordMoreInfo,
                offset = offset,
                length = len,
                group = group
            )
            index_!!.add(newItem)

            offset += len;
            begin = wordEndPos + 1
        }
    }

    private fun readReplace() {
        var str = readFileString("assets/replace.txt")
        if (str == null) return
        str = str.replace("\r\n", "\n")
        val arr = str.split("\n")
        replace_ = mutableListOf()
        arr.forEach { line->
            val pos = line.indexOf(' ')
            if (pos >= 0) {
                val key = line.substring(0, pos)
                val value = line.substring(pos + 1).trim()
                if (value != null && value.isNotEmpty()) {
                    replace_!!.add(KV<String>(
                        key = key,
                        value = value
                    ))
                }
            }
        }
    }

    private fun readRedirect() {
        val str_redirect_files = getConfig("redirect")
        if (str_redirect_files?.isNotEmpty() == true) {
            val files = str_redirect_files.split(",")
            redirect_ = mutableListOf()
            files.forEach { item->
                val path = "assets" + (
                        if (item.startsWith("/")) item.trim()
                        else "/" + item.trim())
                val str = this.readFileString(path)
                redirect_!!.add(FishDictRedirect(content = str!!))
            }
        }
    }

    private fun getConfig(key: String): String? {
        if (config_ == null) return null
        try {
            return config_!!.getString(key)
        } catch (e: Exception) {
            return null
        }
    }

    fun getCount(): Int {
        if (index_ == null) return 0
        return index_!!.size
    }
}